﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Microsoft.Office.OneNote;
using System.Xml.Linq;

namespace OneNoteFrameworkTests
{
    internal static class X
    {
        public static XName Name(string localName)
        {
            return XName.Get(localName, "http://schemas.microsoft.com/office/onenote/2007/onenote");
        }

        public static XDocument Doc(params object[] content)
        {
            return new XDocument(new XDeclaration("1.0", null, null), content);
        }

        public static XElement E(string localName, params object[] content)
        {
            return new XElement(Name(localName), content);
        }

        public static XAttribute A(string name, object value)
        {
            return new XAttribute(XName.Get(name), value);
        }
    }

    internal sealed class TestIdGenerator
    {
        private int _lastId;

        public OneNoteObjectId Next()
        {
            return OneNoteObjectId.Parse(
                String.Format("{0:B}{0}{1}", 
                new Guid(++_lastId, 0, 0, new byte[8]), "{1}", "{T0}"));
        }
    }

    [TestFixture]
    public class ConflictHandlingTests
    {

        private void CreateDummyIdObjectWithConflict(out DummyObject dummy, out DummyObject server_dummy)
        {
            dummy = new DummyObject();
            dummy.Name = "FirstName";

            XDocument xdoc = XDocument.Parse(@"<Dummy Name=""SecondName"" 
                                                      xmlns=""http://schemas.microsoft.com/office/onenote/2007/onenote""
                                               />");

            // this simulates an Update() call

            OneNoteSchema.Deserialize(dummy, xdoc.Root);
            Assert.IsTrue(dummy.HasConflicts);
            Assert.AreEqual("FirstName", dummy.Name);

            server_dummy = OneNoteObject.GetServerView(dummy);
            Assert.IsNotNull(server_dummy);
            Assert.IsFalse(server_dummy.HasConflicts);
            Assert.AreEqual("SecondName", server_dummy.Name);

            try
            {
                server_dummy.Name = "this should fail!!";
                Assert.Fail("Properties should be read-only on server view objects.");
            }
            catch (InvalidOperationException)
            {
            }

            try
            {
                server_dummy.RevertChanges();
                Assert.Fail("RevertChanges should fail on server view objects.");
            }
            catch (InvalidOperationException)
            {
            }
        }

        private static void AssertObjectNames(DummyContainerObject container, params string[] names)
        {
            IEnumerable<DummyIdObject> dummies = container.GetObjects();
            Assert.IsNotNull(dummies);

            int i = 0;

            foreach (DummyIdObject dummy in dummies)
            {
                Assert.IsNotNull(dummy);
                Assert.AreNotEqual(OneNoteObjectId.Empty, dummy.ObjectId);
                // Assert.IsFalse(dummy.HasConflicts);

                Assert.Less(i, names.Length);
                Assert.AreEqual(names[i], dummy.Name);

                i++;
            }
        }

        private DummyContainerObject CreateDummyContainer(params string[] names)
        {
            XDocument doc = CreateDummyContainerXml(names);
                
            DummyContainerObject container = new DummyContainerObject();
            OneNoteSchema.Deserialize(container, doc.Root);
            Assert.IsFalse(container.HasConflicts);

            return container;
        }

        private static XDocument CreateDummyContainerXml(params string[] names)
        {
            var ids = new TestIdGenerator();

            XDocument doc = X.Doc(
                X.E("Container", from n in names
                                 select X.E("DummyID",
                                    X.A("ID", ids.Next()),
                                    X.A("Name", n))
                                 )
                );
            return doc;
        }
 
        [Test]
        public void RevertBasicConflict()
        {
            DummyObject dummy, server_dummy;
            CreateDummyIdObjectWithConflict(out dummy, out server_dummy);

            dummy.RevertChanges();

            Assert.AreEqual("SecondName", dummy.Name);
            Assert.AreEqual("SecondName", server_dummy.Name);
        }

        [Test]
        public void AcceptBasicConflict()
        {
            DummyObject dummy, server_dummy;
            CreateDummyIdObjectWithConflict(out dummy, out server_dummy);

            dummy.AcceptChanges();

            Assert.IsFalse(dummy.HasConflicts);

            Assert.AreEqual("FirstName", dummy.Name);
            Assert.AreEqual("SecondName", server_dummy.Name); // maybe the server version shouldn't be 
                                                              // accessible anymore?
        }

        [Test]
        public void RevertElementPropertyConflict()
        {
            OuterDummyObject outer = new OuterDummyObject();
            outer.Inner.Name = "First Value";

            Assert.IsFalse(outer.HasConflicts);
            Assert.IsFalse(outer.Inner.HasConflicts);

            XDocument xdoc = XDocument.Parse(@"<Outer xmlns=""http://schemas.microsoft.com/office/onenote/2007/onenote"">
                                                   <Dummy Name=""Updated Value"" />
                                               </Outer>");

            OneNoteSchema.Deserialize(outer, xdoc.Root);

            Assert.IsTrue(outer.HasConflicts);
            Assert.IsTrue(outer.Inner.HasConflicts);

            OuterDummyObject server_outer = OneNoteObject.GetServerView(outer);
            Assert.IsNotNull(server_outer);
            Assert.IsFalse(server_outer.HasConflicts);

            DummyObject server_inner = server_outer.Inner;
            Assert.IsNotNull(server_inner);
            Assert.IsFalse(server_inner.HasConflicts);
            Assert.AreEqual("Updated Value", server_inner.Name);


            outer.RevertChanges();

            Assert.AreEqual("Updated Value", outer.Inner.Name);
        }

        [Test]
        public void UpdateChildObjectNoConflict()
        {
            DummyContainerObject container = CreateDummyContainer("First", "Second");
            Assert.IsFalse(container.HasConflicts);
            
            AssertObjectNames(container, "First", "Second");

            XDocument updates = CreateDummyContainerXml("First-Updated", "Second-Updated");
            OneNoteSchema.Deserialize(container, updates.Root);

            Assert.IsFalse(container.HasConflicts);

            AssertObjectNames(container, "First-Updated", "Second-Updated");
        }

        [Test]
        public void UpdateChildObjectConflict()
        {
            DummyContainerObject container = CreateDummyContainer("First", "Second", "Third");
            Assert.IsFalse(container.HasConflicts);

            AssertObjectNames(container, "First", "Second", "Third");

            DummyIdObject[] objs = container.GetObjects().ToArray();

            objs[0].Name = "First-Altered";
            objs[1].Name = "Second-Altered";
            objs[2].Name = "Third-Altered";

            XDocument document = CreateDummyContainerXml("First-Updated", "Second-Updated", "Third-Updated");
            OneNoteSchema.Deserialize(container, document.Root);

            AssertObjectNames(container, "First-Altered", "Second-Altered", "Third-Altered");

            Assert.IsTrue(container.HasConflicts);
            Assert.That(objs[0].HasConflicts);
            Assert.That(objs[1].HasConflicts);
            Assert.That(objs[2].HasConflicts);

            DummyContainerObject server_container = OneNoteObject.GetServerView(container);
            AssertObjectNames(server_container, "First-Updated", "Second-Updated", "Third-Updated");
        }
    }

    [OneNoteXmlElement("Container")]
    public sealed class DummyContainerObject : OneNoteObject, IOneNoteObjectWithChildren
    {
        private readonly OneNoteObjectStorage<DummyIdObject> _storage;

        public DummyContainerObject()
        {
            _storage = new OneNoteObjectStorage<DummyIdObject>();
            SetInnerObjects(_storage);
        }

        public IEnumerable<DummyIdObject> GetObjects()
        {
            return _storage.OfType<DummyIdObject>();
        }

        void IOneNoteObjectWithChildren.UpdateFrom(IEnumerable<XElement> elements)
        {
            _storage.UpdateFrom(elements);
        }
    }

    [OneNoteXmlElement("Dummy")]
    public sealed class DummyObject : OneNoteObject
    {
        internal static readonly OneNoteProperty NameProperty = OneNoteProperty.Register(
            "Name", typeof(String), typeof(DummyObject));

        public string Name
        {
            get { return GetValue<String>(NameProperty); }
            set { SetValue(NameProperty, value); }
        }
    }


    [OneNoteXmlElement("DummyID")]
    [OneNoteObjectIdAttribute("ID")]
    public sealed class DummyIdObject : OneNoteObjectWithIdentity
    {
        internal static readonly OneNoteProperty NameProperty = OneNoteProperty.Register(
            "Name", typeof(String), typeof(DummyIdObject));

        public DummyIdObject(OneNoteObjectId id)
            : base(id)
        {

        }

        public string Name
        {
            get { return GetValue<String>(NameProperty); }
            set { SetValue(NameProperty, value); }
        }
    }

    [OneNoteXmlElement("Outer")]
    public sealed class OuterDummyObject : OneNoteObject
    {
        internal static readonly OneNoteProperty InnerProperty = OneNoteProperty.Register(
            "Inner", typeof(DummyObject), typeof(OuterDummyObject),
            DefaultValueProvider.NewInstance<DummyObject>());

        public DummyObject Inner
        {
            get { return GetValue<DummyObject>(InnerProperty); }
        }
    }
}
